package de.uni_potsdam.hpi.bpt.ai.diagram;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * @author Philipp
 * Helper class to build an abstract diagram structure out of json
 */
public class DiagramBuilder {
	
	private final static String jsonPattern = "glossary://(.*?)/([\\w\\W]*?)(;;)";
	
	public static Diagram parseJson(String json) throws JSONException {
		return parseJson(json, false);
	}
	
	/**
	 * Parse the json string to the diagram model,
	 * assumes that the json is hierarchical ordered
	 * @param json
	 * @return	Model with all shapes defined in JSON
	 * @throws JSONException
	 */
	public static Diagram parseJson(String json, Boolean keepGlossaryLink) throws JSONException {
		JSONObject modelJSON = new JSONObject(json);
		return parseJson(modelJSON, keepGlossaryLink);
	}	

	public static Diagram parseJson(JSONObject json) throws JSONException {
		return parseJson(json, false);
	}
	
	/**do the parsing on an JSONObject,
	 * assumes that the json is hierarchical ordered,
	 * so all shapes are reachable over child relations
	 * @param json	hierarchical JSON object 
	 * @return	Model with all shapes defined in JSON
	 * @throws JSONException
	 */
	public static Diagram parseJson(JSONObject json, Boolean keepGlossaryLink) throws JSONException {
		HashMap<String, JSONObject> flatJSON = flatRessources(json);
		
		String id="canvas";
		
		if (json.has("resourceId")) {
			id=json.getString("resourceId");
		}
		
		Diagram diagram = new Diagram(id);

		parseStencilSet(json, diagram);
		parseSsextensions(json,diagram);
		
		ShapeFactory shapeFactory = new ShapeFactory(diagram.getStencilset(), diagram.getSsextensions());
		
		// first find all shapes to use them as children/parent/incoming/outgoing 
		for (String resourceId : flatJSON.keySet()) {
			JSONObject modelJSON = flatJSON.get(resourceId);
			shapeFactory.getShape(modelJSON);
		}
		
		// now parse resources of all shapes
		for (String resourceId : flatJSON.keySet()) {
			JSONObject modelJSON = flatJSON.get(resourceId);
			parseRessource(modelJSON, keepGlossaryLink, shapeFactory);
		}
		
		parseStencil(json, diagram, shapeFactory);
		parseProperties(json, diagram, keepGlossaryLink);
		parseChildShapes(json, diagram, shapeFactory);
		parseBounds(json, diagram);
		
		return diagram;
	}

	/**Parse one resource to a shape object and add it to the shapes array
	 * 
	 * @param shapes
	 * @param flatJSON
	 * @param resourceId
	 * @throws JSONException
	 */
	private static void parseRessource(
			JSONObject modelJSON,
			Boolean keepGlossaryLink,
			ShapeFactory shapeFactory) throws JSONException {
		
		Shape current = shapeFactory.getShape(modelJSON);
		
		parseStencil(modelJSON, current, shapeFactory);
		parseProperties(modelJSON, current, keepGlossaryLink);
		parseOutgoings(modelJSON, current, shapeFactory);
		parseChildShapes(modelJSON, current, shapeFactory);
		parseDockers(modelJSON, current);
		parseBounds(modelJSON, current);
		parseTarget(modelJSON, current, shapeFactory);
	}
	
	/** parse the stencil out of a JSONObject and set it to the current shape
	 * @param modelJSON
	 * @param current
	 * @throws JSONException
	 */
	private static void parseStencil(JSONObject modelJSON, Shape current, ShapeFactory shapeFactory)
			throws JSONException {
		// get stencil type
		if (modelJSON.has("stencil")) {
			JSONObject stencil = modelJSON.getJSONObject("stencil");
			// TODO other attributes of stencil
			String stencilString = "";
			if (stencil.has("id")) {
				stencilString = stencil.getString("id");
			}
			current.setStencil(new StencilType(stencilString, shapeFactory.getStencilSet()));
		}
	}

	/** crates a StencilSet object and add it to the current diagram
	 * @param modelJSON
	 * @param current
	 * @throws JSONException
	 */
	private static void parseStencilSet(JSONObject modelJSON, Diagram current)
			throws JSONException {
		// get stencil type
		if (modelJSON.has("stencilset")) {
			JSONObject object = modelJSON.getJSONObject("stencilset");
			String url = null;
			String namespace = null;
			
			boolean noSpecification = true;
			
			if (object.has("url"))
				url = object.getString("url");

			if (object.has("namespace")){
				namespace = object.getString("namespace");
				
				try {
					current.setStencilset(StencilSetFactory.getInstance().getStencilSet(namespace));
					noSpecification = false;
				} catch (UnsupportedEncodingException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (FileNotFoundException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
				if (noSpecification) {
					StencilSet ss = new StencilSet(url, namespace);
					current.setStencilset(ss);
				}
				
			}
		}
	}

	/**create a HashMap form the json properties and add it to the shape
	 * @param modelJSON
	 * @param current
	 * @throws JSONException
	 */
	@SuppressWarnings("unchecked")
	private static void parseProperties(JSONObject modelJSON, Shape current, Boolean keepGlossaryLink)
			throws JSONException {
		if (modelJSON.has("properties")) {
			JSONObject propsObject = modelJSON.getJSONObject("properties");
			Iterator<String> keys = propsObject.keys();
			Pattern pattern = Pattern.compile(jsonPattern);
	
			while (keys.hasNext()) {
				StringBuilder result = new StringBuilder();
				int lastIndex = 0;
				String key = keys.next();
				String value=propsObject.getString(key);
				
//				System.out.println(value);
//				System.out.println(keepGlossaryLink);
				
				if(!keepGlossaryLink) {
					Matcher matcher = pattern.matcher(value);
					while(matcher.find()) {
						String id = matcher.group(1);
						current.addGlossaryIds(id);
						String text = matcher.group(2);
						result.append(text);
						lastIndex = matcher.end();
					}
					result.append(value.substring(lastIndex));
					value=result.toString();
				}
				
				current.putProperty(key, value);
			}
		}
	}

	/** adds all json extension to an diagram
	 * @param modelJSON
	 * @param current
	 * @throws JSONException
	 */
	private static void parseSsextensions(JSONObject modelJSON, Diagram current)
			throws JSONException {
		if (modelJSON.has("ssextensions")) {
			JSONArray array = modelJSON.getJSONArray("ssextensions");
			for (int i = 0; i < array.length(); i++) {
				current.addSsextension(array.getString(i));
			}
		}
	}

	/** parse the outgoings form an json object and add all shape references to
	 * the current shapes, add new shapes to the shape array
	 * @param shapes
	 * @param modelJSON
	 * @param current
	 * @throws JSONException
	 */
	private static void parseOutgoings(
			JSONObject modelJSON,
			Shape current,
			ShapeFactory shapeFactory) throws JSONException {
		
		if (modelJSON.has("outgoing")) {
			ArrayList<Shape> outgoings = new ArrayList<Shape>();
			JSONArray outgoingObject = modelJSON.getJSONArray("outgoing");
			for (int i = 0; i < outgoingObject.length(); i++) {
				Shape out = shapeFactory.getShape(outgoingObject.getJSONObject(i));
				outgoings.add(out);
				out.addIncoming(current);
			}
			if (outgoings.size() > 0)
				current.setOutgoings(outgoings);
		}
	}

	/** creates a shape list containing all child shapes and set it to the current shape
	 * new shape get added to the shape array
	 * @param shapes
	 * @param modelJSON
	 * @param current
	 * @throws JSONException
	 */
	private static void parseChildShapes(
			JSONObject modelJSON, 
			Shape current,
			ShapeFactory shapeFactory) throws JSONException {
		
		if (modelJSON.has("childShapes")) {
			ArrayList<Shape> childShapes = new ArrayList<Shape>();

			JSONArray childShapeObject = modelJSON.getJSONArray("childShapes");
			for (int i = 0; i < childShapeObject.length(); i++) {
				childShapes.add(shapeFactory.getShape(childShapeObject.getJSONObject(i)));
			}
			if (childShapes.size() > 0){
				for(Shape each:childShapes)
					each.setParent(current);
				current.setChildShapes(childShapes);};
		}
	}

	/**creates a point array of all dockers and add it to the current shape
	 * @param modelJSON
	 * @param current
	 * @throws JSONException
	 */
	private static void parseDockers(JSONObject modelJSON, Shape current)
			throws JSONException {
		if (modelJSON.has("dockers")) {
			ArrayList<Point> dockers = new ArrayList<Point>();

			JSONArray dockersObject = modelJSON.getJSONArray("dockers");
			for (int i = 0; i < dockersObject.length(); i++) {
				Double x = dockersObject.getJSONObject(i).getDouble("x");
				Double y = dockersObject.getJSONObject(i).getDouble("y");
				dockers.add(new Point(x, y));

			}
			if (dockers.size() > 0)
				current.setDockers(dockers);

		}
	}

	/**creates a bounds object with both point parsed from the json and 
	 * set it to the current shape
	 * @param modelJSON
	 * @param current
	 * @throws JSONException
	 */
	private static void parseBounds(JSONObject modelJSON, Shape current)
			throws JSONException {
		if (modelJSON.has("bounds")) {
			JSONObject boundsObject = modelJSON.getJSONObject("bounds");
			current.setBounds(new Bounds(new Point(boundsObject.getJSONObject(
					"lowerRight").getDouble("x"), boundsObject.getJSONObject(
					"lowerRight").getDouble("y")), new Point(boundsObject
					.getJSONObject("upperLeft").getDouble("x"), boundsObject
					.getJSONObject("upperLeft").getDouble("y"))));
		}
	}

	/** parse the target resource and add it to the current shape
	 * @param shapes
	 * @param modelJSON
	 * @param current
	 * @throws JSONException
	 */
	private static void parseTarget(
			JSONObject modelJSON,
			Shape current,
			ShapeFactory shapeFactory) throws JSONException {
		if (modelJSON.has("target")) {
			JSONObject targetObject = modelJSON.getJSONObject("target");
			if (targetObject.has("resourceId")) {
				current.setTarget(
					shapeFactory.getShape(targetObject));
			}
		}
	}


	/**
	 * Prepare a model JSON for analyze, resolves the hierarchical structure
	 *  creates a HashMap which contains all resourceIds as keys and for each key
	 *  the JSONObject, all id are keys of this map
	 * @param object
	 * @return a HashMap keys: all ressourceIds values: all child JSONObjects
	 * @throws JSONException
	 */
	public static HashMap<String, JSONObject> flatRessources(JSONObject object)
			throws JSONException {
		HashMap<String, JSONObject> result = new HashMap<String, JSONObject>();

		// no cycle in hierarchies!!
		if (object.has("resourceId") && object.has("childShapes")) {
			result.put(object.getString("resourceId"), object);
			JSONArray childShapes = object.getJSONArray("childShapes");
			for (int i = 0; i < childShapes.length(); i++) {
				result.putAll(flatRessources(childShapes.getJSONObject(i)));
			}

		}
		;

		return result;
	}
}
